iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0
Modern Web

終究都要學 React 何不現在學呢?系列 第 16

終究都要學 React 何不現在學呢? - React 進階 - useRef - (16)

  • 分享至 

  • xImage
  •  

前言

接下來要來聊一個有趣的 Hook,也就是 useRef 這個 Hook,這個 Hook 有什麼特別的功能呢?就讓我們瞧瞧吧!

useRef

前面這邊先提一下 useRef 這個 Hook 稍微有一點特別而且有趣,為什麼會特別且有趣呢?這邊先讓我們看一下 useRef 基本寫法

const ref = useRef(initialValue);

基本上 useRef 用法與前面的 Hook 都差不多,但是要稍微注意的事情是 useRef 會回傳一個物件,而這個物件會有一個 current 的屬性,這個 current 屬性就是我們要存放的值,這邊先來看一個簡單的範例

const App = () => {
  const ref = React.useRef(0);
  console.log(ref); // { current: 0 }

  return (
    <div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

很有趣吧?useRef 明明傳入的是一個單純的 Number 0,但是卻回傳一個 { current: 0 } 物件,那麼這是為什麼呢?這邊讓我們翻一下 React Hook 原始碼

function useRef<T>(initialValue: T): {current: T} {
  currentlyRenderingComponent = resolveCurrentlyRenderingComponent();
  workInProgressHook = createWorkInProgressHook();
  const previousRef = workInProgressHook.memoizedState;
  if (previousRef === null) {
    const ref = {current: initialValue};
    if (__DEV__) {
      Object.seal(ref);
    }
    workInProgressHook.memoizedState = ref;
    return ref;
  } else {
    return previousRef;
  }
}

上面這一段稍微可能有一點複雜,所以我們先稍微精簡一下變成以下這樣

function useRef<T>(initialValue: T): {current: T} {
  // ... 忽略其他程式碼
  if (previousRef === null) {
    const ref = {current: initialValue}; // 核心重點
    // ... 忽略其他程式碼
    return ref;
  } else {
    // ... 忽略其他程式碼
  }
}

我們可以看到回傳物件的原因就是 React 處理的,因此這也是為什麼 useRef 會回傳一個物件。

除此之外 useRef 並不會觸發 re-render,因此可以拿來存放一些不會變動的值,但是你要注意一下,請不要這樣子撰寫

const App = () => {
  let ref = React.useRef(0);

  React.useEffect(() => {
    ref += 1;
  }, []);

  return (
    <div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

如果你如上方這樣子撰寫的話,其實你 console.log(ref) 之後會看到 [object Object]1,這樣子是錯誤的,因為 ref 是一個物件,所以你要這樣子撰寫才正確

const App = () => {
  let ref = React.useRef(0);

  React.useEffect(() => {
    ref.current += 1;
    console.log(ref); // { current: 1 }
  }, []);

  return (
    <div>
    </div>
  );
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

那麼這邊拉回到剛剛前面所提的「useRef 並不會觸發 re-render」這件事情,我們是如何知道畫面不會被重新 re-render 呢?其實驗證方式很簡單,將 ref 渲染在畫面上,然後寫個 setTimeout 去累加就可以知道畫面會不會 re-render

const App = () => {
  const ref = React.useRef(0);

  React.useEffect(() => {
    setTimeout(() => {
      ref.current += 1;
    }, 2000)
  }, []);

  return (
    <div>
      { ref.current }
    </div>
  );
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

我們可以看到上述範例中,畫面並沒有被重新 re-render,因此我們可以確定 useRef 並不會觸發 re-render。

那麼因為這個特性關係,因此我們可以藉由此特性來監測畫面是否有被 re-render,而這邊我們必須搭配 useEffect 來使用,因為 useEffect 會在每次 re-render 後被觸發,所以這邊舉例一範例來說明。

const App = () => {
  const [count, setCount] = React.useState(0);
  const ref = React.useRef(0);

  const fn =() => {
    setCount((pre) => pre + 1);
  }

  const showRef = () => {
    console.log(`當前畫面以 re-render ${ ref.current} 次`);
  }

  React.useEffect(() => {
    ref.current += 1;
  });

  return (
    <div>
      <p>count:{ count }</p>
      <button type="button" className="bg-blue-400 p-4 text-white hover:bg-blue-700" onClick={ fn }>點我</button>|
      <button type="button" className="bg-blue-400 p-4 text-white hover:bg-blue-700" onClick={ showRef }>顯示已渲染的次數</button> 
    </div>
  )
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

我們可以看到上述範例中,當我們點擊「點我」按鈕時,畫面上的 count:0 會被重新 re-render 成 count:1,隨著我們點擊幾次,畫面上就會跟著呈現相對應得 count,而當我們點擊「顯示已渲染的次數」按鈕時,我們可以看到畫面已經被重新 re-render 了 N 次,因為 useEffect 會在每次 re-render 後被觸發,所以這邊我們可以藉由 useRef 來監測畫面是否有被 re-render,但你會發現不管怎麼樣 red.current 永遠都是比當前畫面的 count 多一次,這原因主要是發生在初始 React 所導致的。

已經看到大半篇的 useRef 介紹,但實質上來講感覺 useRef 好像在開發上很弱、很沒用對吧?所以這邊我們就來插個花,先來聊一下 Vue 的部分。

還記得你在初學 Vue 的時候都是如何選取 DOM 的嗎?忘了也沒關係,這邊提供範例程式碼讓你回憶一下

<div id="app">
  <p class="p">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure commodi consectetur cumque nesciunt dolorem deleniti ipsum atque modi libero consequuntur, porro labore autem quod! Voluptatibus eos eveniet optio nemo atque.</p>
</div>
const { createApp, onMounted } = Vue;

const app = createApp({
  setup() {
    onMounted(() => {
      console.log('DOM:', document.querySelector('.p'));
    })
  }
});

app.mount('#app');

CodePen 連結

上面就是一個很簡單的 Vue 選取 DOM 方式。

但這種方式並不是很方便畢竟要寫很長的 document.querySelector,所以 Vue 也提供了 ref 來幫助我們選取 DOM

<div id="app">
  <p ref="p">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure commodi consectetur cumque nesciunt dolorem deleniti ipsum atque modi libero consequuntur, porro labore autem quod! Voluptatibus eos eveniet optio nemo atque.</p>
</div>
const { createApp, onMounted, ref } = Vue;

const app = createApp({
  setup() {
    const p = ref(null);

    onMounted(() => {
      console.log('DOM:', p.value);
    })

    return {
      p,
    }
  }
});

app.mount('#app');

CodePen 連結

兩者寫法攤開來看就可以看出兩者的差異,那麼接著拉回到 React 的部分。

相信你看到上面的範例之後,大概就很清楚 useRef 在實戰上還可以用於選取 DOM 了吧?所以就來示範一下吧

const App = () => {
  const pRef = React.useRef(null);

  React.useEffect(() => {
    console.log('DOM:', pRef.current);
  });

  return (
    <div>
      <p ref={ pRef }>Lorem ipsum dolor sit amet consectetur adipisicing elit. Obcaecati ad assumenda laborum id alias? Reiciendis fugiat, explicabo, natus aperiam debitis facilis quia consequatur voluptatum veniam nobis nulla ad perspiciatis in.  </p>
    </div>
  )
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

CodePen 連結

除此之外 useRef 也可以用來選取元件,但是這邊我就不額外示範了,而是保留給你自己嘗試看看囉。

後記

本文將會同步更新到我的部落格


上一篇
終究都要學 React 何不現在學呢? - React 進階 - useReducer - (15)
下一篇
終究都要學 React 何不現在學呢? - React 進階 - Custom Hook - (17)
系列文
終究都要學 React 何不現在學呢?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言